Skip to content

Batch fix: #11-#16, #18 (untested, needs live device pass) - #18

Draft
dubpixel wants to merge 8 commits into
mainfrom
fix/action-plan-batch
Draft

Batch fix: #11-#16, #18 (untested, needs live device pass)#18
dubpixel wants to merge 8 commits into
mainfrom
fix/action-plan-batch

Conversation

@dubpixel

@dubpixel dubpixel commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Summary

Implements everything from ACTION-PLAN.md that's verifiable without live hardware:

Not done here (need live device access)

Test plan

🤖 Generated with Claude Code

dubpixel and others added 8 commits September 5, 2026 11:52
Found 12.1 GiB of already-expired-but-uncollected artifacts via the API,
deleted them manually, and logged the recurring fix (delete CI artifacts
after they land in a release) as a housekeeping item.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Card was single-width at 17px monospace, which wraps mid-octet on a
full IPv4 address (word-break:break-all). Matches the existing
Hostname card's span-2 treatment for another field with unpredictable
length.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
companion-dashboard's main.js checks process.argv.includes('--kiosk-mode'),
not '--kiosk' -- the wrong flag meant kiosk mode (fullscreen + the
auto-started :80 web server) never actually triggered. Confirmed against
upstream source, not guessed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Depends on #13's fix landing first -- companion-dashboard only
auto-starts its /control web server (port 80) when real kiosk mode
is detected. Link only shown while the kiosk service is actually
active (dashboard_enabled() == svc_active), same gating as the
existing Toggle Fullscreen button.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
toggle_net()/pin_static() -- the functions the deck's NET key drives --
called write_networkd_config() unconditionally, writing systemd-networkd
files and restarting systemd-networkd. On Raspberry Pi OS, which ships
NetworkManager by default, that does nothing durable: NetworkManager
keeps managing the interface and reasserts its own connection profile's
DHCP setting on next boot, which is exactly the "has to be reselected
every boot" behavior reported.

The manual /network web form already branched on nmcli_available() vs
networkd correctly -- the deck-driven functions never got the same
treatment. Added write_nmcli_config() (nmcli connection modify, which
persists straight to the on-disk profile) and an apply_net_config()
dispatcher, and pointed toggle_net()/pin_static() at the dispatcher
instead of the networkd-only function directly.

get_current_net_mode() in dpx-deck-splash.py already reads live kernel
state via `ip addr`, so it correctly reflects whichever backend actually
applied the change -- no read-side fix needed, this was write-path only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
#11, #12)

Two related gaps, one mechanism, per the design decision to solve them
together rather than as separate bolted-on fixes:

- #11: nothing brought dpx-deck-splash.service back once a mode service's
  own Restart=on-failure exhausted its StartLimitBurst -- the deck just
  went dark/stale forever. Fixed with OnFailure=dpx-deck-splash.service
  drop-ins on all three mode units (as .service.d/ overrides, not direct
  edits, since they ship from vendor .deb packages). OnFailure= only
  fires once a unit's ActiveState actually reaches "failed" -- systemd
  holds it in "activating (auto-restart)" between individual retries --
  so this is inherently once-per-real-outage, not once-per-retry.
  Event-driven, no polling.

- #12: dpx-deck-splash.service and the persisted mode service were both
  WantedBy=multi-user.target, racing at boot with Conflicts= picking
  whichever won -- confirmed nondeterministic on hardware ("sometimes
  splash wins and blocks it without a GO press"). Fixed with a new
  dpx-mode-select.service oneshot that reads /etc/dpx-mode at boot and
  starts exactly that one service, falling back to the splash only if
  nothing's persisted or the target refuses to start. Splash is no
  longer auto-enabled on its own, so there's nothing left to race.

Not live-verified yet -- no device access this pass. Needs a real
boot-cycle test and a forced-permanent-failure test once hardware is
available; also worth rechecking the "GO does nothing" symptom against
this fix, since execute_staged()'s existing mode_dead check already
looks correct on paper and may have been a downstream effect of the same
race rather than its own bug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two layers: release-action.yaml's release job now deletes its own run's
CI artifacts right after they land in the GitHub Release (they're
redundant once the .img.gz is a real release asset). New weekly
artifact-sweep.yaml workflow catches everything else -- stray feature-
branch/debug builds that never go through the release job -- by deleting
anything already past its own expires_at, since GitHub's own cleanup can
lag by weeks in practice (confirmed this session: found and manually
cleared 12.1GiB that was up to three weeks overdue).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…10)

Root cause found via live SSH investigation on dpx-buttonode-2199
(rockpi-s), confirmed with three isolated tests:

1. udev_retrigger() alone (what switch_mode() actually called) does NOT
   recreate /dev/hidraw* once a libusb consumer (Buttons/Satellite/
   deck-splash) has detached the kernel driver to claim the device --
   verified by running it in isolation and confirming hidraw stayed
   missing.
2. The full unbind/bind (usb_power_cycle(), already built and already
   wired into the manual /power-cycle-deck button) DOES recreate it --
   verified live.
3. With hidraw present, Companion picks up the Stream Deck cleanly at
   startup ("Surface panel ready: streamdeck:..."). Companion's surface
   module only scans once at startup and never retries, so if hidraw is
   missing at that exact moment, Companion silently finds nothing --
   this is why the reported symptom looked intermittent/mode-switch-
   specific rather than a permissions problem.

Fix: switch_mode() now calls the same usb_power_cycle() fallback
/power-cycle-deck already used manually, instead of the gentle-only
udev_retrigger(). usb_power_cycle() already tries the gentle retrigger
first and only escalates to the disruptive unbind/bind if that alone
wasn't enough, so this is a safe drop-in with no added cost in the
common case.

Verified end-to-end on real hardware: forced hidraw missing via
deck-splash, ran the actual --apply-mode companion production path with
the fix applied, hidraw came back automatically, Companion started and
opened the surface panel successfully.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant